Einheit 2 — Ein Nachrichtenformat für alle Kanäle
Was du nach dieser Einheit weißt: Du kannst ein einheitliches Format für eingehende Nachrichten festlegen, doppelte Verarbeitung zuverlässig verhindern und sicherstellen, dass nur berechtigte Personen den Assistenten nutzen.
Warum ein eigenes Format?
Jeder Messenger liefert Nachrichten anders an. Nextcloud Talk schickt ein verschachteltes Objekt, in dem der Text als JSON-String steckt. Synology Chat sendet Formularfelder. Microsoft Teams liefert eine „Activity“ mit Dutzenden Feldern.
Würde der Router all diese Varianten kennen müssen, wäre er unlesbar — und jeder neue Messenger hieße, den Router umzubauen. Deshalb übersetzt der Eingang jede Nachricht zuerst in ein einheitliches Format. Alles dahinter kennt nur noch dieses Format.
Nextcloud Talk ─┐
Teams ──────────┼──→ [ Eingang je Kanal ] ──→ Einheitsformat ──→ [ Router ] → …
Synology Chat ──┤
Web-Formular ───┘
Das Einheitsformat
{
"kanal": "nextcloud",
"nachricht_id": "48211",
"unterhaltung_id": "k7x2mqpa",
"absender_id": "m.mueller",
"absender_name": "Maria Müller",
"text": "Wie lief der Umsatz im Oktober?",
"datei": null,
"antwort_auf": "48211"
}
| Feld | Bedeutung |
|---|---|
kanal | Woher die Nachricht kommt — bestimmt später, über welchen Weg die Antwort zurückgeht |
nachricht_id | Eindeutige ID der Nachricht im Messenger. Pflicht — darauf beruht der Schutz vor Doppelverarbeitung |
unterhaltung_id | Der Chat, Kanal oder Raum. Die Antwort geht genau dorthin zurück |
absender_id | Technische ID der Person im Messenger, nicht der Anzeigename |
absender_name | Anzeigename, nur für Texte |
text | Der reine Nachrichtentext, ohne Formatierung und ohne Erwähnungen des Bots |
datei | Bei Dateien ein Objekt mit id, name, mime, groesse, sonst null |
antwort_auf | Worauf die Antwort im Messenger verweisen soll (Thread, Zitat) |
Schreib das Format in die Beschreibung des Router-Workflows. Wer später einen neuen Kanal anschließt, weiß dann genau, was der Eingang liefern muss.
Doppelte Verarbeitung verhindern
Messenger stellen Nachrichten manchmal mehrfach zu — etwa wenn dein Workflow nicht schnell genug mit 200 OK geantwortet hat und der Messenger es erneut versucht. Ohne Schutz legt der Assistent dann zwei Tickets an oder setzt zwei Erinnerungen.
Der Schutz ist einfach: Jede nachricht_id wird beim ersten Mal in einer Tabelle vermerkt. Ist sie schon da, wird die Nachricht verworfen.
CREATE TABLE chat_nachricht (
nachricht_id TEXT PRIMARY KEY,
kanal TEXT,
absender_id TEXT,
text TEXT,
erstellt TEXT
);
Der Router liest zu Beginn, ob die ID schon bekannt ist:
SELECT COUNT(*) AS schon FROM chat_nachricht WHERE nachricht_id = '{{ nachricht_id }}'
und vermerkt sie, sobald er entschieden hat:
INSERT OR IGNORE INTO chat_nachricht (nachricht_id, kanal, absender_id, text, erstellt)
VALUES ('{{ nachricht_id }}', '{{ kanal }}', '{{ absender_id }}', '{{ text_sql }}', datetime('now'))
INSERT OR IGNORE sorgt dafür, dass ein zweiter Eintrag mit derselben ID einfach übergangen wird, statt einen Fehler zu erzeugen. Dieses Prinzip heißt Idempotenz: Egal, wie oft dieselbe Nachricht ankommt — sie wirkt nur einmal.
Ein Apostroph im Nachrichtentext („Wie geht's?“) beendet sonst die SQL-Zeichenkette — im harmlosen Fall gibt es einen Fehler, im schlimmen Fall wird fremder SQL-Code ausgeführt. Ersetze einfache Anführungszeichen im JavaScript Agent durch das typografische Zeichen ’, bevor der Text in eine SQL-Vorlage kommt, und kürze lange Texte:
function sqlText(v, max) {
var s = (v === undefined || v === null) ? '' : String(v);
s = s.split("'").join('’').slice(0, max || 1000);
return s.trim() ? s : '-'; // leere Texte vermeiden, siehe unten
}
Der Ersatz durch ’ ist robuster als das in SQL übliche Verdoppeln (''): Leere Zeichenketten und verdoppelte Anführungszeichen führen im Internal Storage Agent leicht zu Fehlern, die ohne Antwort enden. Deshalb ersetzt die Funktion auch leere Texte durch einen Bindestrich.
In den Beispielen dieses Kurses heißen solche vorbereiteten Felder …_sql, zum Beispiel text_sql.
Wer darf den Assistenten nutzen?
Ein Assistent, der Tickets anlegt oder Umsätze nennt, darf nicht jedem antworten, der zufällig im selben Chat ist. Lege eine Tabelle der berechtigten Personen an:
CREATE TABLE assistent_nutzer (
kanal TEXT,
absender_id TEXT,
name TEXT,
rolle TEXT, -- z. B. 'admin', 'vertrieb', 'gast'
aktiv INTEGER,
PRIMARY KEY (kanal, absender_id)
);
Der Router prüft jede Nachricht gegen diese Tabelle. Unbekannte Absender bekommen eine höfliche, aber nichtssagende Antwort — und keine Auskunft darüber, was der Assistent könnte.
Prüfe immer die technische ID (absender_id), nie den Anzeigenamen. Anzeigenamen kann jeder ändern.
Die Spalte rolle wird wichtig, sobald mehrere Personen den Assistenten nutzen. Im Fähigkeiten-Katalog (Einheit 4) kannst du dann festlegen, welche Rolle welchen Skill nutzen darf — etwa dass nur die Geschäftsführung Umsatzfragen stellen darf.
Der Eingang als JavaScript Agent
Für jeden Kanal übersetzt ein kleiner JavaScript Agent die Rohdaten ins Einheitsformat. Das Grundgerüst ist immer gleich:
Agent.receive = function() {
var p = this.incomingMessage().payload || {};
// 1. Nur echte Nutzernachrichten durchlassen (keine Bots, keine Systemmeldungen)
// 2. Felder aus dem kanalspezifischen Format lesen
// 3. Ins Einheitsformat übertragen
var o = {
kanal: 'web',
nachricht_id: String(p.nachricht_id || ''),
unterhaltung_id: String(p.unterhaltung_id || 'web'),
absender_id: String(p.absender_id || ''),
absender_name: String(p.absender_name || ''),
text: String(p.text || '').trim(),
datei: null,
antwort_auf: String(p.nachricht_id || '')
};
// Ohne ID keine Verarbeitung: sonst greift der Schutz vor Doppelverarbeitung nicht
if (!o.nachricht_id || !o.absender_id) { return; }
this.createMessage(o);
};
Wie die Schritte 1 und 2 für Nextcloud Talk, Synology Chat und Microsoft Teams aussehen, zeigt Kurs 8. In den Übungen dieses Kurses kommt die Nachricht aus einem Web Form Agent — die nachricht_id erzeugst du dort einfach aus dem aktuellen Zeitstempel.
Kurzer Selbst-Check
Ein Messenger stellt dieselbe Nachricht dreimal zu. Wie oft legt der Assistent ein Ticket an?
Einmal. Beim ersten Mal ist die nachricht_id unbekannt, der Router verarbeitet die Nachricht und vermerkt die ID. Beim zweiten und dritten Mal liefert die Abfrage schon = 1, und der Router bricht ab.
Warum prüft man die Berechtigung im Router und nicht im Eingang?
Weil die Prüfung dann nur einmal gebaut werden muss — für alle Kanäle. Der Eingang übersetzt nur, der Router entscheidet.